package weeny.face;

import static java.awt.event.ActionEvent.ACTION_PERFORMED;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;

import weeny.util.logs.Debug;

/**
 * A simple control widget to let you tune numbers. It is basically a wrap of
 * JSpinner. You can add ActionListener to this widget. The listeners will be
 * invoked in some cases, please see {@link #addActionListener(ActionListener)}.
 * 
 * The spinner model can take four arguments: default value, min value, max
 * value, and step value. But the min/max value can be null, which means there
 * is no upper or lower bound. Otherwise, when the value is changed to something
 * not in the proper range, it will be reset to the min or max value.
 * 
 * @author weeny
 */
public class LabeledSpinner extends Box{
	private JLabel label;
	private JSpinner spinner;
	
	protected KeyAdapter keyListener = new KeyAdapter(){
		@Override
		public void keyPressed(KeyEvent e) {
			if (e.getKeyCode() != 10) {
				return;
			}
			Object current = spinner.getValue();
			JTextField fld = (JTextField) spinner.getEditor().getComponent(0);
			try {
				spinner.commitEdit();
			} catch (ParseException e1) {
				Debug.prob("Illegal "+fld.getText()+", restore it");
				fld.setText(current.toString());
				spinner.setValue(current);
			}
			Comparable value = (Comparable) spinner.getValue();
			SpinnerModel model = spinner.getModel();
			if (model instanceof SpinnerNumberModel) {
				SpinnerNumberModel snm = (SpinnerNumberModel) model;
				Comparable max = snm.getMaximum();
				Comparable min = snm.getMinimum();
				if (max != null && max.compareTo(value)<0){
					value = max;
					Debug.info("Above max: "+max);
				}
				if (min != null && min.compareTo(value)>0){
					value = min;
					Debug.info("Below min: "+min);
				}
			}
			fireActionEvent(e.getModifiers());
		}
	};
	protected void fireActionEvent(int modifier){
		ActionEvent ae = new ActionEvent(this, ACTION_PERFORMED, "", modifier);
		for(ActionListener al : listeners){
			al.actionPerformed(ae);
		}
	}
	protected MouseAdapter mouseListener = new MouseAdapter(){
		@Override
		public void mouseReleased(MouseEvent e) {
			if (e.getButton() == 1)
				fireActionEvent(e.getModifiers());
		}
	};
	/**
	 * Get the current value in the spinner
	 */
	public Number getValue(){
		return (Number) spinner.getValue();
	}
	/**
	 * Default constructor
	 * @see #LabeledSpinner(String)
	 */
	public LabeledSpinner() {
		this("label");
	}
	/**
	 * Use the passed-in String as the label, and use integer no-limits model
	 * behind the spinner
	 */
	public LabeledSpinner(String label){
		super(BoxLayout.X_AXIS);
		spinner = new JSpinner();
		spinner.getComponent(0).addMouseListener(mouseListener);
		spinner.getComponent(1).addMouseListener(mouseListener);
		this.setBorder(null);
		this.label = new JLabel(label);
		this.add(Box.createHorizontalGlue());
		this.add(this.label);
		this.add(spinner);
		//by default it is using integer model without max and min values
		this.setSpinnerModel(new SpinnerNumberModel());
	}
	/**
	 * Use the passed-in String as the label. The spinner has the default value
	 * dft. When click the arrow, the value will be changed by step. Please
	 * notice that dft and step should have the same type, otherwise it will
	 * throw out an exception 
	 */
	public LabeledSpinner(String label, Number dft, Number step){
		this(label);
		checkTypes(dft, step);
		this.setSpinnerModel(dft, step);
	}
	/**
	 * Set the label, default value, moving step, and minimun value of the
	 * spinner, @see {@link #LabeledSpinner(String, Number, Number)}.
	 * @see #setMinValue(Comparable)
	 */
	public LabeledSpinner(String label, Number dft, Number step, Number min){
		this(label, dft, step);
		checkTypes(dft, step, min);
		this.setMinValue((Comparable) min);
	}
	
	/**
	 * Set the value displayed in the interface.
	 * @param num
	 */
	public void setValue(Number num){
		checkTypes((Number) spinner.getValue(), (Number)num);
		((SpinnerNumberModel)spinner.getModel()).setValue(num);	
	}
	/**
	 * Set the mininum value of the spinner, Once the minimun value is set, 
	 * whenever you set the value smaller than min, it will be reset to min
	 * automatically.
	 */
	public void setMinValue(Comparable min){
		checkTypes((Number) spinner.getValue(), (Number)min);
		((SpinnerNumberModel)spinner.getModel()).setMinimum(min);
	}
	/**
	 * Set the maximun value of the spinner, Once the maximun value is set, 
	 * whenever you set the value bigger than max, it will be reset to max
	 * automatically.
	 */
	public void setMaxValue(Comparable max){
		checkTypes((Number) spinner.getValue(), (Number)max);
		((SpinnerNumberModel)spinner.getModel()).setMaximum(max);
	}
	/**
	 * Directly init this spinner with a specific model
	 */
	public LabeledSpinner(String label, SpinnerNumberModel model){
		this(label);
		this.setSpinnerModel(model);
	}
	/**
	 * Directly set the model to this spinner
	 */
	public void setSpinnerModel(SpinnerNumberModel model){
		spinner.getEditor().getComponent(0).removeKeyListener(keyListener);
		spinner.setModel(model);
		spinner.getEditor().getComponent(0).addKeyListener(keyListener);
	}
	/**
	 * Directly set the model to this spinner using this default value and step
	 */
	public void setSpinnerModel(Number dft, Number step){
		checkTypes(dft, step);
		SpinnerNumberModel snm = new SpinnerNumberModel(dft, null, null, step);
		this.setSpinnerModel(snm);
	}
	protected List<ActionListener> listeners = new ArrayList<ActionListener>();
	
	/**
	 * Add a listener to this spinner, the added listeners are notified when:
	 *   1) release your mouse (left button) on the arrow button, OR
	 *   2) Hit the enter key in the field box
	 */
	public void addActionListener(ActionListener lsn){
		if (!listeners.contains(lsn)) {
			listeners.add(lsn);
		}
	}
	/**
	 * Remove a listener from this spinner
	 */
	public boolean removeListener(ActionListener lsn){
		return listeners.remove(lsn);
	}
	//check all the passed-in numbers are of the same type
	private void checkTypes(Number...nums){
		Class type = null;
		for (Number n : nums) {
			if (type == null) {
				type = n.getClass();
			}
			else if (type != n.getClass()){
				throw new IllegalArgumentException("Type does not match, " +
						n.getClass()+" v.s. "+type);
			}
		}
	}
}